﻿using System;
using System.Collections.Generic;
using System.Xml;
using System.IO;
using System.Text;
using System.Reflection;
using System.ComponentModel;
using ArgusPCLib.Collections;

namespace ArgusPCLib.Xml
{
	public class Serializer : IDisposable
	{
		internal const string NameElement = "Element";
		internal const string NameReference = "Reference";
		internal const string NameType = "Type";
		internal const string PropertyBackingField = ">k__BackingField";

		static readonly Type[] SkipTypes = new Type[] { typeof(IntPtr) };

		XmlWriter xmlWriter;
		ListPair<string,object> path;

		public Serializer(XmlWriter xmlWriter, string rootName)
		{
			this.xmlWriter = xmlWriter;
			this.xmlWriter.WriteStartDocument();
			this.xmlWriter.WriteStartElement(XmlConvert.EncodeName(rootName));
			this.path = new ListPair<string, object>();
		}

		public static Serializer FromStream(Stream stream, string rootName)
		{
			XmlWriterSettings settings = new XmlWriterSettings();
			settings.Indent = true;
			return Serializer.FromStream(stream, rootName, settings);
		}

		public static Serializer FromStream(Stream stream, string rootName, XmlWriterSettings settings)
		{
			XmlWriter xmlWriter = XmlWriter.Create(stream, settings);
			Serializer serializer = new Serializer(xmlWriter, rootName);
			return serializer;
		}

		public void Serialize(object obj)
		{
			Type type = obj.GetType();
			this.Serialize(type.Name, obj, true);
		}

		public void Serialize(string name, object obj)
		{
			this.Serialize(name, obj, true);
		}

		private void Serialize(string name, object obj, bool writeTypeName)
		{
			Type type = obj.GetType();
			name = XmlConvert.EncodeLocalName(name);
			if (this.WriteAsReference(name, obj) == true)
				return;

			this.xmlWriter.WriteStartElement(name);
			if (writeTypeName == true)
				this.xmlWriter.WriteAttributeString(NameType, type.AssemblyQualifiedName);

			this.path.Add(type.Name, obj);

			
			if (obj is ISerializable)
			{
				ISerializable serializable = (ISerializable)obj;
				serializable.Serialize(this);
			}
			else if (Serializer.SkipTypes.Contains(type) == false)
			{
				Func<string, object> parser = ArgusPCLib.Reflection.Parser.GetParseHandler(type);
				if (parser != null)
				{
					string s = obj.ToStringInvariant();
					this.xmlWriter.WriteString(s);
				}
				else if (type.IsArray == true)
				{
					System.Collections.IEnumerable enumerable = (System.Collections.IEnumerable)obj;
					foreach (object element in enumerable)
					{
						this.Serialize(NameElement, element, false);
					}
				}
				else
				{
					IEnumerable<FieldInfo> fis = type.GetRuntimeFields();

					foreach (FieldInfo fi in fis)
					{
						if (fi.IsStatic)
							continue;

						object val = fi.GetValue(obj);
						string fieldName;
						if (fi.Name.EndsWith(PropertyBackingField) == true)
							fieldName = fi.Name.Substring(1, fi.Name.Length - PropertyBackingField.Length - 1);
						else
							fieldName = fi.Name;
						if (val != null)
							this.Serialize(fieldName, val, false);
					}
				}
			}

			this.xmlWriter.WriteEndElement();
			this.xmlWriter.Flush();

			this.path.RemoveLast();
		}

		private bool WriteAsReference(string name, object obj)
		{
			StringBuilder referencePath = new StringBuilder();
			for (int i = 0; i < this.path.Count; i++)
			{
				referencePath.Append('.' + this.path.List1[i]);
				if (Object.ReferenceEquals(this.path.List2[i], obj) == true)
				{
					referencePath.Remove(0, 1);
					this.WriteReference(name, referencePath.ToString());
					return true;
				}
			}
			return false;
		}

		private void WriteReference(string name, string referencePath)
		{
			this.xmlWriter.WriteStartElement(name);
			this.xmlWriter.WriteAttributeString(NameReference, referencePath);
			this.xmlWriter.WriteEndElement();
		}

		public void Close()
		{
			this.xmlWriter.WriteEndElement();
			this.xmlWriter.WriteEndDocument();
			this.xmlWriter.Flush();
			this.xmlWriter.Dispose();
		}

		void IDisposable.Dispose()
		{
			this.Close();
		}
	}
}
